# Copyright (c) 2007-2009 Andrew Price
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. The name of the author may not be used to endorse or promote products
#    derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""Provides a simple interface to the Twitter API"""

import socket
import urllib
import urllib2
import base64
import httplib

class TwitterException(Exception):

	"""A generic Exception class for when things go wrong wrt Twitter"""

	pass # Nothing special about this exception

class TwitterAuthException(TwitterException):

	"""An exception to raise when Twitter auth fails"""

	pass # Nothing special about this exception

class Twitter:
	"""
	Provides a simple interface to the Twitter API
	"""

	def __init__(self):
		"""
		Initialise a Twitter API interface
		"""
		self.baseurl = u'http://api.t.sina.com.cn/'
		self.auth = ""
		self.agent = "twyt-derived"
		self.set_timeout()

	def set_timeout(self, timeout=20):
		"""
		Set the connection timeout period in seconds (default 20).
		"""
		self.timeout = int(timeout)
		#socket.setdefaulttimeout(self.timeout)

	def set_user_agent(self, agent):
		"""
		Sets the user agent which is sent to Twitter in the
		X-Twitter-Client header
		"""
		self.agent = str(agent)

	def set_auth(self, user, pwd):
		"""
		Set the user's Twitter credentials
		"""
		str64 = base64.encodestring('%s:%s' % (user,pwd))[:-1]
		self.auth = u'Basic %s' % str64

	def __request(self, data, handler, doauth=False, method='GET'):
		"""
		Sends a request to the given remote handler. The data
		argument must be a list of pairs of strings.
		"""
		url = self.baseurl + handler
		req = None
		if method == 'GET':
			url = url + '?' + urllib.urlencode(data)
			req = urllib2.Request(url)
		elif method == 'POST':
			req = urllib2.Request(url, urllib.urlencode(data))

		req.add_header(u'X-Twitter-Client', self.agent)

		if doauth:
			if not self.auth:
				raise TwitterAuthException("No user/password specified")
	
			req.add_header(u'Authorization', self.auth)

		result = ""
		try:
			handle = urllib2.urlopen(req)
			result = handle.read()
		except urllib2.HTTPError, e:
			if e.code == 401:
				raise TwitterException("Bad username/password")
			else:
				raise TwitterException(str(e))
		except urllib2.URLError, e:
			if type(e.reason) == str:
				raise TwitterException("Connect failed: " + e.reason)
			else:
				raise TwitterException("Connect failed: " + str(e.reason))
		except httplib.BadStatusLine, e:
			raise TwitterException(e.message)

		return result

	def post(self, data, handler):
		"""
		Sends a POST request to the given remote handler. The data
		argument must be a list of pairs of strings.
		"""
		return self.__request(data, handler, doauth=True, method='POST')


	def get(self, data, handler, doauth=False):
		"""
		Sends a GET request to the given remote handler.  The data
		argument must be a list of pairs of strings.
		"""
		return self.__request(data, handler, doauth=doauth, method='GET')


	def status_update(self, msg="", in_reply_to=None):
		"""
		Updates the user's status on twitter, otherwise known as
		tweeting. in_reply_to (optional) explicitly tells Twitter which
		status ID this message is a reply to.
		"""
		data = []
		msg = msg.strip()
#		if len(msg.decode('utf-8')) > 140:
#			raise TwitterException(
#				"Message too long (%d chars, max 140)" % len(msg.decode('utf-8')))

		if len(msg) <= 0:
			raise TwitterException("Message too short")

		data = [("status", msg), ("source", self.agent)]

		if in_reply_to:
			data.append(("in_reply_to_status_id", in_reply_to))

		return self.post(data, u'statuses/update.json')
	
	def status_repost(self, id, msg=""):
		"""
		Updates the user's status on twitter, otherwise known as
		tweeting. in_reply_to (optional) explicitly tells Twitter which
		status ID this message is a reply to.
		"""
		data = []
		msg = msg.strip()
		if len(msg) > 140:
			raise TwitterException(
				"Message too long (%d chars, max 140)" % len(msg))

		if len(msg) <= 0:
			raise TwitterException("Message too short")

		data = [("id", str(id)), ("status", msg), ("source", self.agent)]

		return self.post(data, u'statuses/repost.json')	

	def status_public_timeline(self):
		"""
		Returns the 20 most recent statuses from non-protected users
		who have set a custom user icon. Does not require authentication.
		"""
		handler = u'statuses/public_timeline.json'

		return self.get([], handler, doauth=False)
		

	def status_friends_timeline(self, since="", since_id="", page=1, count=None):
		"""
		Returns the 20 most recent statuses posted in the last 24
		hours from the authenticating user and that user's friends.
		"""

		try:
			if page and int(page) < 1:
				raise TwitterException("Page number is out of "\
						"range: " + str(page))
		except ValueError:
			raise TwitterException("Invalid page number: " +\
					str(page))
		
		handler = u'statuses/friends_timeline.json'
		data = []
		if since:
			data.append(("since", since))
		if since_id:
			data.append(("since_id",since_id))
		if page:
			data.append(("page", page))
		if count:
			data.append(("count", count))

		return self.get(data, handler, doauth=True)

	def status_user_timeline(self, id="", count=20, since="", since_id=None,
			page=None):
		"""
		Returns the 20 (or count) most recent statuses posted by the
		authenticating user. It's also possible to request another
		user's timeline using id.
		"""
		handler = u'statuses/user_timeline'
		if id != "":
			handler += u'/' + id + u'.json'
		else:
			handler += u'.json'

		data = []
		if since:
			data.append(("since", since))
		if since_id:
			data.append(("since_id", since_id))
		if page:
			data.append(("page", page))

		if 0 < count <= 20:
			data.append(("count", count))
		else:
			raise TwitterException(
				"'count' parameter out of range. Must be between 1 and 20.")

		return self.get(data, handler, doauth=True)

	def status_show(self, id):
		"""
		Returns a single status, specified by the id parameter.
		"""
		if not id:
			raise TwitterException("No ID specified")

		handler = u'statuses/show/%s.json' % id

		return self.get([], handler, doauth=True)

	def status_replies(self, page=None, since="", since_id=None):
		"""
		Returns the 20 most recent replies (status updates prefixed with
		@username posted by users who are friends with the user being
		replied to). The page argument gets the Nth 20 replies.
		"""
		data = []
		if page:
			data = [("page",page)]
		if since:
			data.append(("since", since))
		if since_id:
			data.append(("since_id", since_id))

		handler = u'statuses/replies.json'

		return self.get(data, handler, doauth=True)

	def status_destroy(self, id):
		"""
		Destroys the status specified by the required ID parameter.
		The authenticating user must be the author of the specified
		status.
		"""
		if int(id) < 0:
			raise TwitterException("ID out of range")

		handler = u'statuses/destroy/%d.json' % id

		return self.post([], handler)

		
	def direct_messages(self, since="", since_id="", page=None):
		"""
		Returns a list of the 20 most recent direct messages sent to the
		authenticating user.
		"""
		try:
			if page and int(page) < 1:
				raise TwitterException("Page number is out of "\
						"range: " + str(page))
		except ValueError:
			raise TwitterException("Invalid page number: " +\
					str(page))
		
		handler = u'direct_messages.json'
		data = []
		if since:
			data.append(("since", since))
		if since_id:
			data.append(("since_id",since_id))
		if page:
			data.append(("page", page))

		return self.get(data, handler, doauth=True)


	def direct_sent(self, since="", since_id="", page=None):
		"""
		Returns a list of the 20 most recent direct messages sent by the
		authenticating user.
		"""
		try:
			if page and int(page) < 1:
				raise TwitterException("Page number is out of "\
						"range: " + str(page))
		except ValueError:
			raise TwitterException("Invalid page number: " +\
					str(page))
		
		handler = u'direct_messages/sent.json'
		data = []
		if since:
			data.append(("since", since))
		if since_id:
			data.append(("since_id",since_id))
		if page:
			data.append(("page", page))

		return self.get(data, handler, doauth=True)

	def direct_new(self, user, text):
		"""
		Sends a new direct message to the specified user from the
		authenticating user.  Requires both the user and text
		parameters.
		"""
		user = user.strip()
		if not user:
			raise TwitterException("User not specified")

		text = text.strip()
		if len(text) > 140:
			raise TwitterException(
				"Message too long (%d chars, max 140)" % len(text))

		if len(text) <= 0:
			raise TwitterException("Message too short")

		data = [("user", user), ("text", text)]

		return self.post(data, u'direct_messages/new.json')


	def direct_destroy(self, id):
		"""
		Destroys the direct message specified in the required ID
		parameter.  The authenticating user must be the recipient of the
		specified direct message.
		"""
		if int(id) < 0:
			raise TwitterException("ID out of range")

		handler = u'direct_messages/destroy/%d.json' % id

		return self.post([], handler)

	def user_friends(self, id=None, page=None, since=""):
		"""
		Returns up to 100 of the authenticating user's friends who have
		most recently updated, each with current status inline.  It's
		also possible to request another user's recent friends list via
		the id parameter.
		"""
		if id is not None and id < 0:
			raise TwitterException("User ID out of range")

		handler = u'statuses/friends.json'

		data = []
		if id:
			data.append(("id", id))
		if page:
			data.append(("page", page))
		if since:
			data.append(("since", since))

		return self.get(data, handler, doauth=True)

	def user_followers(self, id=None, page=None):
		"""
		Returns the authenticating user's followers, each with current
		status. If id is given, returns the followers of the user
		matching id (screen name or user id).
		"""
		data = []
		if page:
			data.append(("page", page))

		handler = u'statuses/followers'

		if id:
			handler += u'/%s' % id

		handler += u'.json'

		return self.get(data, handler, doauth=True)

	def user_show(self, id="", email=""):
		"""
		Returns extended information of a given user, specified by ID or
		screen name with the id parameter.  This information includes
		design settings, so third party developers can theme their
		widgets according to a given user's preferences.  The id
		parameter should be the ID or username of a user.
		"""
		data = []
		handler = u'users/show'
		if id:
			handler += u'/%s.json' % id
		elif email:
			handler += u'.json'
			data = [("email", email)]

		return self.get(data, handler, doauth=True)

	def block_create(self, id=None):
		"""
		Blocks the user specified in the ID parameter as the
		authenticating user.  Returns the blocked user when successful.
		"""
		if id is None:
			raise TwitterException("User ID not specified")

		handler = u'blocks/create/%s.json' % str(id)

		return self.post([], handler)


	def block_destroy(self, id=None):
		"""
		Un-blocks the user specified in the ID parameter as the
		authenticating user.  Returns the un-blocked user when
		successful.
		"""
		if id is None:
			raise TwitterException("User ID not specified")

		handler = u'blocks/destroy/%s.json' % str(id)

		return self.post([], handler)

	def friendship_create(self, id, follow=False):
		"""
		Befriends the user specified in the ID parameter as the
		authenticating user.  Returns the befriended user when
		successful.  Returns a string describing the failure condition
		when unsuccessful. If follow is True, notifications will be
		explicitly turned on for this user.
		"""
		handler = u'friendships/create/%s.json' % id

		data = []
		if follow:
			data = [("follow", follow)]

		return self.post(data, handler)

	def friendship_destroy(self, id):
		"""
		Discontinues friendship with the user specified in the ID
		parameter as the authenticating user.  Returns the un-friended
		user when successful.  Returns a string describing the failure
		condition when unsuccessful.
		"""
		handler = u'friendships/destroy/%s.json' % id

		return self.post([], handler)

	def friendship_exists(self, user_a, user_b):
		"""
		Tests if a friendship exists between two users. Returns True or
		False.
		"""
		handler = u'friendships/exists.json'
		
		data = [("user_a", user_a), ("user_b", user_b)]
		
		return self.get(data, handler, doauth=True)

	def account_rate_limit_status(self, withauth=True):
		"""
		Returns the remaining number of API requests available to the
		requesting user before the API limit is reached for the current
		hour. Calls to rate_limit_status do not count against the rate
		limit.  If authentication credentials are provided, the rate
		limit status for the authenticating user is returned.
		Otherwise, the rate limit status for the requester's IP address
		is returned.
		"""
		handler = u'account/rate_limit_status.json'
		return self.get([], handler, doauth=withauth)

	def social_graph_friends_ids(self, id=None):
		"""
		Returns an array of numeric IDs for every user the specified
		user is following.
		"""
		if id is not None and id < 0:
			raise TwitterException("User ID out of range")

		handler = u'friends/ids.json'

		data = []
		if id:
			data.append(("id", id))

		return self.get(data, handler, doauth=True)

	def social_graph_followers_ids(self, id=None):
		"""
		Returns an array of numeric IDs for every user the specified
		user is followed by.
		"""
		if id is not None and id < 0:
			raise TwitterException("User ID out of range")

		handler = u'followers/ids.json'

		data = []
		if id:
			data.append(("id", id))

		return self.get(data, handler, doauth=True) 
